package storage

import (
	"crypto/hmac"
	"crypto/sha1"
	"encoding/base64"
	"encoding/json"
	"fmt"
	"io"
	"io/ioutil"
	"net/http"
	"os"
	"path/filepath"
	"strconv"
	"time"

	"github.com/aliyun/aliyun-oss-go-sdk/oss"
	"github.com/sirupsen/logrus"
	"github.com/xuelang-group/suanpan-appconnector/pkg/common"
	"github.com/xuelang-group/suanpan-appconnector/pkg/config"
	"github.com/xuelang-group/suanpan-appconnector/pkg/util"
)

const TOKEN_UPDATE_BEFORE_SECOND int = 1800

type OssStorage struct {
	StorageOssEndpoint    string `mapstructure:"--storage-oss-endpoint" default:"http://oss-cn-beijing.aliyuncs.com"`
	StorageOssBucketName  string `mapstructure:"--storage-oss-bucket-name" default:"suanpan"`
	StorageOssAccessId    string `mapstructure:"--storage-oss-access-id"`
	StorageOssAccessKey   string `mapstructure:"--storage-oss-access-key"`
	StorageOssTempStore   string `mapstructure:"--storage-oss-temp-store"`
	StorageOssGlobalStore string `mapstructure:"--storage-oss-global-store"`
	UseStsToken           bool
}

type Credentials struct {
	SecurityToken   string `json:"SecurityToken"`
	AccessKeyId     string `json:"AccessKeyId"`
	AccessKeySecret string `json:"AccessKeySecret"`
	Expiration      string `json:"Expiration"`
}

type StsTokenResp struct {
	Success     bool        `json:"success"`
	Msg         string      `json:"msg"`
	RequestId   string      `json:"RequestId"`
	Credentials Credentials `json:"Credentials"`
}

var stsTokenResp *StsTokenResp

func (o *OssStorage) getBucket() (*oss.Bucket, error) {
	var cli *oss.Client

	if o.UseStsToken {
		resp, err := GetStsTokenResp(o.StorageOssAccessKey)
		if err != nil {
			return nil, err
		}

		cli, err = oss.New(o.StorageOssEndpoint, resp.Credentials.AccessKeyId,
			resp.Credentials.AccessKeySecret, oss.SecurityToken(resp.Credentials.SecurityToken))
		if err != nil {
			return nil, err
		}
	} else {
		var err error
		cli, err = oss.New(o.StorageOssEndpoint, o.StorageOssAccessId,
			o.StorageOssAccessKey)
		if err != nil {
			return nil, err
		}
	}

	bucket, err := cli.Bucket(o.StorageOssBucketName)
	if err != nil {
		//log.Errorf("Get oss bucket error: %w", err)
		return nil, err
	}

	return bucket, nil
}

func convertStorageParemterToOssOptions(param *[]StorageParemeter, opts *[]oss.Option) error {
	if param == nil || opts == nil {
		return nil
	}
	for _, p := range *param {
		for k, v := range p {
			switch k {
			case "ContentType":
				*opts = append(*opts, oss.ContentType(v))
			case "CacheControl":
				*opts = append(*opts, oss.CacheControl(v))
			case "Expires":
				i, err := strconv.Atoi(v)
				if err != nil {
					logrus.Error(err)
				}
				t := time.Now().Add(time.Duration(i) * time.Second)
				*opts = append(*opts, oss.Expires(t))
			default:
				return fmt.Errorf("minio paremter:%s, not supported yet", k)
			}
		}
	}
	return nil
}

func (o *OssStorage) FGetObject(objectName, filePath string, param ...StorageParemeter) error {
	bucket, err := o.getBucket()
	if err != nil {
		return err
	}

	opts := []oss.Option{}
	err = convertStorageParemterToOssOptions(&param, &opts)
	if err != nil {
		logrus.Error(err)
	}

	// bug fix
	// heng: 22/2/18 if no directory exist
	// oss will return error because oss use os.OpenFile diretly and report no such file or directory

	// Extract top level directory.
	objectDir, _ := filepath.Split(filePath)
	if objectDir != "" {
		// Create any missing top level directories.
		if err := os.MkdirAll(objectDir, 0700); err != nil {
			return err
		}
	}

	logrus.Debugf("prepare to get object from oss, %s, %s", objectName, filePath)
	return bucket.GetObjectToFile(objectName, filePath, opts...)
}

func (o *OssStorage) FPutObject(objectName, filePath string, param ...StorageParemeter) error {
	bucket, err := o.getBucket()
	if err != nil {
		return err
	}

	opts := []oss.Option{}
	err = convertStorageParemterToOssOptions(&param, &opts)
	if err != nil {
		logrus.Error(err)
	}
	logrus.Debugf("prepare to put object to oss, %s, %s", objectName, filePath)

	return bucket.PutObjectFromFile(objectName, filePath, opts...)
}

func (o *OssStorage) PutObject(objectName string, reader io.Reader, param ...StorageParemeter) error {
	bucket, err := o.getBucket()
	if err != nil {
		return err
	}

	opts := []oss.Option{}
	err = convertStorageParemterToOssOptions(&param, &opts)
	if err != nil {
		logrus.Error(err)
	}

	return bucket.PutObject(objectName, reader, opts...)
}

func (o *OssStorage) ListObjects(objectPrefix string, recursive bool, maxKeys int, param ...StorageParemeter) ([]ObjectItem, error) {
	bucket, err := o.getBucket()
	if err != nil {
		return nil, err
	}

	delimiter := oss.Delimiter(`/`)
	if recursive {
		delimiter = oss.Delimiter(``)
	}

	res, err := bucket.ListObjectsV2(oss.Prefix(objectPrefix), oss.MaxKeys(maxKeys), delimiter)
	if err != nil {
		//log.Errorf("List oss objects error: %w", err)
		return nil, err
	}

	objects := make([]ObjectItem, 0)
	for _, o := range res.Objects {
		objects = append(objects, ObjectItem{
			Name:         o.Key,
			LastModified: util.ISOString(o.LastModified),
		})
	}

	return objects, nil
}

func (o *OssStorage) DeleteObject(objectName string, param ...StorageParemeter) error {
	bucket, err := o.getBucket()
	if err != nil {
		return err
	}

	return bucket.DeleteObject(objectName)
}

func (o *OssStorage) DeleteMultiObjects(objectNames []string, param ...StorageParemeter) error {
	bucket, err := o.getBucket()
	if err != nil {
		return err
	}

	_, err = bucket.DeleteObjects(objectNames)
	return err
}

func GetHeaders(userId string, accessSecret string, version string) map[string][]string {
	env := config.GetEnv()
	headers := make(map[string][]string)
	headers[env.SpUserIdHeaderField] = []string{userId}
	//TODO:check this
	headers[env.SpUserSignatureHeaderField] = []string{signatureV1(accessSecret, userId)}
	headers[env.SpUserSignVersionHeaderField] = []string{version}

	return headers
}

func checkStsTokenRespValid(token *StsTokenResp, validTimeRange int) bool {
	if token == nil {
		return false
	}

	if token.Credentials.Expiration == "" {
		return false
	}
	timeToken, err := time.Parse(time.RFC3339, token.Credentials.Expiration)
	if err != nil {
		logrus.Error(err)
		return false
	}
	if time.Until(timeToken) < time.Duration(validTimeRange*int(time.Second)) {
		return false
	}
	return true
}

// TODO:refactor this funciton
// bad code
func GetStsTokenResp(accessToken string) (*StsTokenResp, error) {
	if accessToken == "" {
		return nil, fmt.Errorf("parameter error, access cannot be nil, please check configs")
	}
	if checkStsTokenRespValid(stsTokenResp, TOKEN_UPDATE_BEFORE_SECOND) {
		return stsTokenResp, nil
	}

	path := `/oss/token`
	req, _ := http.NewRequest("GET", common.GetMasterUrl()+path, nil)
	req.Header = GetHeaders(common.GetUserId(), accessToken, "v1")
	logrus.Debugf("user:%s, token:%s, stsToken request to path:%s, req header:%s",
		common.GetUserId(), accessToken, common.GetMasterUrl()+path, req.Header)

	resp, err := http.DefaultClient.Do(req)
	if err != nil {
		logrus.Errorf("Request sts token error: %s", err.Error())
		return nil, err
	}
	defer resp.Body.Close()

	data, err := ioutil.ReadAll(resp.Body)
	if err != nil {
		logrus.Errorf("Read response body error: %s", err.Error())
		return nil, err
	}

	logrus.Debugf("fetch stsToken complete, code:%s, data:%s", resp.Status, data)
	if resp.StatusCode != 200 {
		err = fmt.Errorf("stsToken request to master failed: %s", resp.Status)
		return nil, err
	}

	if len(data) == 0 {
		return nil, fmt.Errorf("stsToken request to master return nil data")
	}
	err = json.Unmarshal(data, &stsTokenResp)
	if err != nil {
		logrus.Errorf("Unmarshal json format error: %s", err.Error())
	}

	if !stsTokenResp.Success {
		return nil, fmt.Errorf("stsToken request to master failed with msg:%s", stsTokenResp.Msg)
	}
	logrus.Debugf("fetch sts token,req:%s, expires in %s", stsTokenResp.RequestId, stsTokenResp.Credentials.Expiration)

	return stsTokenResp, nil
}
func signatureV1(secret, data string) string {
	mac := hmac.New(sha1.New, []byte(secret))
	mac.Write([]byte(data))

	return base64.StdEncoding.EncodeToString(mac.Sum(nil))
}
